// Copyright (c) 2003 Richard Blewett
//
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included 
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
// THE SOFTWARE.

using System;
using System.Runtime.Remoting;
using System.IO;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Messaging;
using System.Collections;

namespace Sedna.API.Support
{

	/// <summary>
	/// This sink provider is for use on the client side of a remoting conversation
	/// where the HTTP channel is being used in conjunction withthe BinaryFormatter.
	/// In the case where the remoted component is being hosted by ASP.NET rather than 
	/// directly by the HTTP server side channel, errors can be returned that the 
	/// client channel does not cope with. It instead passes the error text to the 
	/// BinaryFormatter who attempts to interpret the message as binary thus producing a 
	/// SerializationException based on incompatable BinaryFormatter versions.
	/// 
	/// The HttpErrorMessageSink examines the stream returned from the request before the 
	/// formatter sees it. If the stream contains a textual error message or HTML then the 
	/// sink throws a remoting exception containing the error message.
	/// 
	/// This sink provider is called by the remoting architecture to create a 
	/// sink that it can insert into the sink chain
	/// </summary>
	public class HttpErrorMessageSinkProvider: IClientChannelSinkProvider 
	{
		// Stores the next provider in the chain
		private IClientChannelSinkProvider next = null;

		// a list of error text to use to match against. This is read from the remoting config
		ArrayList errors = new ArrayList();
		bool useHeaders = true;

		public HttpErrorMessageSinkProvider () 
		{
		}

		public HttpErrorMessageSinkProvider(IDictionary properties, ICollection providerData)
		{
			if( properties["useHeaders"] != null && (string)properties["useHeaders"] == "false" )
			{
				useHeaders = false;

				// process any additional error message matches
				foreach(SinkProviderData data in providerData)
				{
					string s = (string)data.Properties["data"];
					if( s != null )
					{
						errors.Add(s);
					}
				}
			}
		}

		public IClientChannelSink CreateSink(IChannelSender channel, string url, object remoteChannelData)
		{
			// Create the sink passing the addition error message matches
			if( useHeaders )
				return new HttpErrorMessageSink(next.CreateSink(channel,url,remoteChannelData), useHeaders, null);
			else
				return new HttpErrorMessageSink(next.CreateSink(channel,url,remoteChannelData), useHeaders, errors);
		}

		/// <summary>
		/// Gets or sets the next provider in the chain
		/// </summary>
		public IClientChannelSinkProvider Next
		{
			get { return next; }
			set { next = value; }
		}

	}

	/// <summary>
	/// This sink intercepts error messages from ASP.NET to stop the BinaryFormatter erroneously
	/// throwing a Serialization Exception
	/// </summary>
	public class HttpErrorMessageSink : IClientChannelSink
	{
	
		#region IClientChannelSink Members

		public void AsyncProcessRequest(IClientChannelSinkStack sinkStack, IMessage msg, ITransportHeaders headers, System.IO.Stream stream)
		{
			nextSink.AsyncProcessRequest(sinkStack, msg, headers, stream);
		}

		// Known error text to check for
		string[] knownErrors = {"System.Runtime.Remoting.RemotingException", "<HTML>"};
		ArrayList allErrors;
		const string CONTENT_TYPE = "Content-Type";
		const string BINARY_PAYLOAD = "application/octet-stream";

		/// <summary>
		/// Called to process the request. This method chains the request to the next sink 
		/// in the sink chain.
		/// </summary>
		/// <param name="msg">The message object</param>
		/// <param name="requestHeaders">The request headers</param>
		/// <param name="requestStream">The request stream</param>
		/// <param name="responseHeaders">The response headers</param>
		/// <param name="responseStream">The response stream</param>
		public void ProcessMessage(IMessage msg, ITransportHeaders requestHeaders, System.IO.Stream requestStream, out ITransportHeaders responseHeaders, out System.IO.Stream responseStream)
		{
			// Process the call as we are only interested in the result
			nextSink.ProcessMessage(msg, requestHeaders, requestStream, out responseHeaders, out responseStream);

			if( useHeaders )
			{
				foreach(DictionaryEntry header in responseHeaders)
				{
					if((string) header.Key == CONTENT_TYPE )
					{
						if( (string)header.Value == BINARY_PAYLOAD )
						{
							// No errors occurred on server side
							return;
						}
						else
						{
							StreamReader sr = new StreamReader(responseStream);
							throw new RemotingException(sr.ReadToEnd());
						}
					}
				}
			}
			else
			{
				// Create a new stream as the response stream is not seekable. If this was not performed, 
				// the response stream would be in the incorrect place when the message was deserialized
				MemoryStream ms = new MemoryStream();

				//Copy the response stream
				int i = responseStream.ReadByte();
				while( i != -1 )
				{
					ms.WriteByte((byte)i);
					i = responseStream.ReadByte();
				}

				// Reset the seekable stream
				ms.Seek(0, SeekOrigin.Begin);

				StreamReader sr = new StreamReader(ms);

				try
				{
					// compare the first line of the response with all known and configured errors
					string httpResponse = sr.ReadLine();

					if( httpResponse != null )
					{
						foreach( string error in allErrors )
						{
							if( httpResponse.ToUpper().StartsWith(error.ToUpper()) )
							{
								throw new RemotingException(httpResponse + sr.ReadToEnd());
							}
						}
					}
				}
				finally
				{
					// Reset the seekable stream
					ms.Seek(0, SeekOrigin.Begin);
					// replace the reponse stream with the 
					// seekable one which is now in the correct position
					responseStream = ms;
				}
			}
		}

		public void AsyncProcessResponse(IClientResponseChannelSinkStack sinkStack, object state, ITransportHeaders headers, System.IO.Stream stream)
		{
			nextSink.AsyncProcessResponse(sinkStack, state, headers, stream);
		}

		public System.IO.Stream GetRequestStream(IMessage msg, ITransportHeaders headers)
		{
			return nextSink.GetRequestStream(msg, headers);
		}

		public IClientChannelSink NextChannelSink
		{
			get
			{
				return nextSink;
			}
		}

		#endregion

		#region IChannelSinkBase Members

		private IClientChannelSink nextSink;
		private bool useHeaders;

		/// <summary>
		/// Initializes the sink with the next sink and the configured 
		/// additional error message matches
		/// </summary>
		/// <param name="next">Next sink in chain</param>
		/// <param name="errors">Addition error message matches from the remoting config file</param>
		public HttpErrorMessageSink (object next, bool useHeaders, ArrayList errors) 
		{
			nextSink = next as IClientChannelSink;
			this.useHeaders = useHeaders;
			if( !useHeaders )
			{
				allErrors = new ArrayList(knownErrors);
				allErrors.AddRange(errors);
			}
		}


		public System.Collections.IDictionary Properties
		{
			get
			{
				return nextSink.Properties;
			}
		}

		#endregion

	}
}
